--[[
    Finalizing Market:
      Note: Feel free to work on any area and submit
            it as a pull request from your git fork.

      BeniS's Skype: benjiz69

      List:
      * Add offer management:
        - Current Offers
        - Offer History

      * Clean up the interface building
        - Add a new market interface file to handle building?

      * Extend information features
        - Hover over offers for purchase information (balance after transaction, etc)
  ]] Market = {}

local protocol = runinsandbox('marketprotocol')

marketWindow = nil
mainTabBar = nil
displaysTabBar = nil
offersTabBar = nil
selectionTabBar = nil

marketOffersPanel = nil
browsePanel = nil
overviewPanel = nil
itemOffersPanel = nil
itemDetailsPanel = nil
itemStatsPanel = nil
myOffersPanel = nil
currentOffersPanel = nil
offerHistoryPanel = nil
itemsPanel = nil
selectedOffer = {}
selectedMyOffer = {}
lastKnownTab = nil
lastOfferTab = nil

nameLabel = nil
feeLabel = nil
balanceLabel = nil
totalPriceEdit = nil
piecePriceEdit = nil
amountEdit = nil
searchEdit = nil
radioItemSet = nil
selectedItem = nil
offerTypeList = nil
categoryList = nil
subCategoryList = nil
slotFilterList = nil
createOfferButton = nil
buyButton = nil
sellButton = nil
anonymous = nil
filterButtons = {}

buyOfferTable = nil
sellOfferTable = nil
detailsTable = nil
buyStatsTable = nil
sellStatsTable = nil

buyCancelButton = nil
sellCancelButton = nil
buyMyOfferTable = nil
sellMyOfferTable = nil

buyMyHistoryTable = nil
sellMyHistoryTable = nil

refreshTimeout = 0
updateEvent = nil
offerExhaust = {}
marketOffers = {}
marketItems = {}
information = {}
currentItems = {}
lastCreatedOffer = 0
fee = 0
averagePrice = 0

loaded = false

local function isItemValid(item, category, searchFilter)
    if not item or not item.marketData then
        return false
    end

    if not category then
        category = MarketCategory.All
    end
    if item.marketData.category ~= category and category ~= MarketCategory.All then
        return false
    end

    -- filter item
    local slotFilter = false
    if slotFilterList:isEnabled() then
        slotFilter = getMarketSlotFilterId(slotFilterList:getCurrentOption().text)
    end
    local marketData = item.marketData

    local filterVocation = filterButtons[MarketFilters.Vocation]:isChecked()
    local filterLevel = filterButtons[MarketFilters.Level]:isChecked()
    local filterDepot = filterButtons[MarketFilters.Depot]:isChecked()

    if slotFilter then
        if slotFilter ~= 255 and item.thingType:getClothSlot() ~= slotFilter then
            return false
        end
    end
    local player = g_game.getLocalPlayer()
    if filterLevel and marketData.requiredLevel and player:getLevel() < marketData.requiredLevel then
        return false
    end
    if filterVocation and marketData.restrictVocation > 0 then
        local voc = Bit.bit(information.vocation)
        if not Bit.hasBit(marketData.restrictVocation, voc) then
            return false
        end
    end
    if filterDepot and Market.getDepotCount(item.marketData.tradeAs) <= 0 then
        return false
    end
    if searchFilter then
        return marketData.name:lower():find(searchFilter)
    end
    return true
end

local function clearItems()
    currentItems = {}
    Market.refreshItemsWidget()
end

local function clearOffers()
    marketOffers[MarketAction.Buy] = {}
    marketOffers[MarketAction.Sell] = {}
    buyOfferTable:clearData()
    sellOfferTable:clearData()
end

local function clearMyOffers()
    marketOffers[MarketAction.Buy] = {}
    marketOffers[MarketAction.Sell] = {}
    buyMyOfferTable:clearData()
    sellMyOfferTable:clearData()
end

local function clearMyHistory()
    marketOffers[MarketAction.Buy] = {}
    marketOffers[MarketAction.Sell] = {}
    buyMyHistoryTable:clearData()
    sellMyHistoryTable:clearData()
end
local function clearFilters()
    for _, filter in pairs(filterButtons) do
        if filter and filter:isChecked() ~= filter.default then
            filter:setChecked(filter.default)
        end
    end
end

local function clearFee()
    feeLabel:setText('')
    fee = 20
end

local function refreshTypeList()
    offerTypeList:clearOptions()
    offerTypeList:addOption('Buy')

    if Market.isItemSelected() then
        if Market.getDepotCount(selectedItem.item.marketData.tradeAs) > 0 then
            offerTypeList:addOption('Sell')
        end
    end
end

local function addOffer(offer, offerType)
    if not offer then
        return false
    end
    local id = offer:getId()
    local player = offer:getPlayer()
    local amount = offer:getAmount()
    local price = offer:getPrice()
    local timestamp = offer:getTimeStamp()
    local itemName = offer:getItem():getMarketData().name
    local action = offer:getState()

    buyOfferTable:toggleSorting(false)
    sellOfferTable:toggleSorting(false)

    buyMyOfferTable:toggleSorting(false)
    sellMyOfferTable:toggleSorting(false)

    buyMyHistoryTable:toggleSorting(false)
    sellMyHistoryTable:toggleSorting(false)

    if amount < 1 then
        return false
    end
    if offerType == MarketAction.Buy then
        if offer.warn then
            buyOfferTable:setColumnStyle('OfferTableWarningColumn', true)
        end

        local row = nil
        if offer.var == MarketRequest.MyOffers then
            row = buyMyOfferTable:addRow({{
                text = itemName
            }, {
                text = price * amount
            }, {
                text = price
            }, {
                text = amount
            }, {
                text = string.gsub(os.date('%c', timestamp), ' ', '  '),
                sortvalue = timestamp
            }})
        elseif offer.var == MarketRequest.MyHistory then
            row = buyMyHistoryTable:addRow({{
                text = itemName
            }, {
                text = price * amount
            }, {
                text = price
            }, {
                text = amount
            }, {
                text = MarketOfferStateString[action],
                sortvalue = timestamp
            }})
        else
            row = buyOfferTable:addRow({{
                text = player
            }, {
                text = amount
            }, {
                text = price * amount
            }, {
                text = price
            }, {
                text = string.gsub(os.date('%c', timestamp), ' ', '  ')
            }})
        end
        row.ref = id

        if offer.warn then
            row:setTooltip(tr('This offer is 25%% below the average market price'))
            buyOfferTable:setColumnStyle('OfferTableColumn', true)
        end
    else
        if offer.warn then
            sellOfferTable:setColumnStyle('OfferTableWarningColumn', true)
        end

        local row = nil
        if offer.var == MarketRequest.MyOffers then
            row = sellMyOfferTable:addRow({{
                text = itemName
            }, {
                text = price * amount
            }, {
                text = price
            }, {
                text = amount
            }, {
                text = string.gsub(os.date('%c', timestamp), ' ', '  '),
                sortvalue = timestamp
            }})
        elseif offer.var == MarketRequest.MyHistory then
            row = sellMyHistoryTable:addRow({{
                text = itemName
            }, {
                text = price * amount
            }, {
                text = price
            }, {
                text = amount
            }, {
                text = MarketOfferStateString[action],
                sortvalue = timestamp
            }})
        else
            row = sellOfferTable:addRow({{
                text = player
            }, {
                text = amount
            }, {
                text = price * amount
            }, {
                text = price
            }, {
                text = string.gsub(os.date('%c', timestamp), ' ', '  '),
                sortvalue = timestamp
            }})
        end
        row.ref = id

        if offer.warn then
            row:setTooltip(tr('This offer is 25%% above the average market price'))
            sellOfferTable:setColumnStyle('OfferTableColumn', true)
        end
    end

    buyOfferTable:toggleSorting(false)
    sellOfferTable:toggleSorting(false)
    buyOfferTable:sort()
    sellOfferTable:sort()

    buyMyOfferTable:toggleSorting(false)
    sellMyOfferTable:toggleSorting(false)
    buyMyOfferTable:sort()
    sellMyOfferTable:sort()

    buyMyHistoryTable:toggleSorting(false)
    sellMyHistoryTable:toggleSorting(false)
    buyMyHistoryTable:sort()
    sellMyHistoryTable:sort()

    return true
end

local function mergeOffer(offer)
    if not offer then
        return false
    end

    local id = offer:getId()
    local offerType = offer:getType()
    local amount = offer:getAmount()
    local replaced = false

    if offerType == MarketAction.Buy then
        if averagePrice > 0 then
            offer.warn = offer:getPrice() <= averagePrice - math.floor(averagePrice / 4)
        end

        for i = 1, #marketOffers[MarketAction.Buy] do
            local o = marketOffers[MarketAction.Buy][i]
            -- replace existing offer
            if o:isEqual(id) then
                marketOffers[MarketAction.Buy][i] = offer
                replaced = true
            end
        end
        if not replaced then
            table.insert(marketOffers[MarketAction.Buy], offer)
        end
    else
        if averagePrice > 0 then
            offer.warn = offer:getPrice() >= averagePrice + math.floor(averagePrice / 4)
        end

        for i = 1, #marketOffers[MarketAction.Sell] do
            local o = marketOffers[MarketAction.Sell][i]
            -- replace existing offer
            if o:isEqual(id) then
                marketOffers[MarketAction.Sell][i] = offer
                replaced = true
            end
        end
        if not replaced then
            table.insert(marketOffers[MarketAction.Sell], offer)
        end
    end
    return true
end

local function updateOffers(offers)
    if not buyOfferTable or not sellOfferTable then
        return
    end

    balanceLabel:setColor('#bbbbbb')
    selectedOffer[MarketAction.Buy] = nil
    selectedOffer[MarketAction.Sell] = nil

    selectedMyOffer[MarketAction.Buy] = nil
    selectedMyOffer[MarketAction.Sell] = nil

    -- clear existing offer data
    buyOfferTable:clearData()
    buyOfferTable:setSorting(4, TABLE_SORTING_DESC)
    sellOfferTable:clearData()
    sellOfferTable:setSorting(4, TABLE_SORTING_ASC)

    sellButton:setEnabled(false)
    buyButton:setEnabled(false)

    buyCancelButton:setEnabled(false)
    sellCancelButton:setEnabled(false)
    for _, offer in pairs(offers) do
        mergeOffer(offer)
    end
    for type, offers in pairs(marketOffers) do
        for i = 1, #offers do
            addOffer(offers[i], type)
        end
    end
end

local function updateDetails(itemId, descriptions, purchaseStats, saleStats)
    local purchaseOfferStatistic = {}
    local saleOfferStatistic = {}
    if not selectedItem then
        return
    end

    -- update item details
    detailsTable:clearData()
    for k, desc in pairs(descriptions) do
        local columns = {{
            text = getMarketDescriptionName(k) .. ':'
        }, {
            text = desc
        }}
        detailsTable:addRow(columns)
    end

    if not table.empty(saleStats) then
        for i = 1, #purchaseStats do
            table.insert(saleOfferStatistic, OfferStatistic.new(saleStats[i][1], saleStats[i][2], saleStats[i][3], saleStats[i][4], saleStats[i][5], saleStats[i][6]))
        end
    end
    if not table.empty(purchaseStats) then
        for i = 1, #purchaseStats do
            table.insert(purchaseOfferStatistic, OfferStatistic.new(purchaseStats[i][1], purchaseStats[i][2], purchaseStats[i][3], purchaseStats[i][4], purchaseStats[i][5], purchaseStats[i][6]))
        end
    end
    
    -- update sale item statistics
    sellStatsTable:clearData()
    if table.empty(saleStats) then
        sellStatsTable:addRow({{
            text = 'No information'
        }})
    else
        local offerAmount = 0
        local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0
        for _, stat in pairs(saleOfferStatistic) do
            if not stat:isNull() then
                offerAmount = offerAmount + 1
                transactions = transactions + stat:getTransactions()
                totalPrice = totalPrice + stat:getTotalPrice()
                local newHigh = stat:getHighestPrice()
                if newHigh > highestPrice then
                    highestPrice = newHigh
                end
                local newLow = stat:getLowestPrice()
                -- ?? getting '0xffffffff' result from lowest price in 9.60 cipsoft
                if (lowestPrice == 0 or newLow < lowestPrice) and newLow ~= 0xffffffff then
                    lowestPrice = newLow
                end
            end
        end

        if offerAmount >= 5 and transactions >= 10 then
            averagePrice = math.round(totalPrice / transactions)
        else
            averagePrice = 0
        end

        sellStatsTable:addRow({{
            text = 'Total Transations:'
        }, {
            text = transactions
        }})
        sellStatsTable:addRow({{
            text = 'Highest Price:'
        }, {
            text = highestPrice
        }})

        if totalPrice > 0 and transactions > 0 then
            sellStatsTable:addRow({{
                text = 'Average Price:'
            }, {
                text = math.floor(totalPrice / transactions)
            }})
        else
            sellStatsTable:addRow({{
                text = 'Average Price:'
            }, {
                text = 0
            }})
        end

        sellStatsTable:addRow({{
            text = 'Lowest Price:'
        }, {
            text = lowestPrice
        }})
    end

    -- update buy item statistics
    buyStatsTable:clearData()
    if table.empty(purchaseOfferStatistic) then
        buyStatsTable:addRow({{
            text = 'No information'
        }})
    else
        local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0
        for _, stat in pairs(purchaseOfferStatistic) do
            if not stat:isNull() then
                transactions = transactions + stat:getTransactions()
                totalPrice = totalPrice + stat:getTotalPrice()
                local newHigh = stat:getHighestPrice()
                if newHigh > highestPrice then
                    highestPrice = newHigh
                end
                local newLow = stat:getLowestPrice()
                -- ?? getting '0xffffffff' result from lowest price in 9.60 cipsoft
                if (lowestPrice == 0 or newLow < lowestPrice) and newLow ~= 0xffffffff then
                    lowestPrice = newLow
                end
            end
        end

        buyStatsTable:addRow({{
            text = 'Total Transations:'
        }, {
            text = transactions
        }})
        buyStatsTable:addRow({{
            text = 'Highest Price:'
        }, {
            text = highestPrice
        }})

        if totalPrice > 0 and transactions > 0 then
            buyStatsTable:addRow({{
                text = 'Average Price:'
            }, {
                text = math.floor(totalPrice / transactions)
            }})
        else
            buyStatsTable:addRow({{
                text = 'Average Price:'
            }, {
                text = 0
            }})
        end

        buyStatsTable:addRow({{
            text = 'Lowest Price:'
        }, {
            text = lowestPrice
        }})
    end
end

local function updateSelectedItem(widget)
    selectedItem.item = widget.item
    selectedItem.ref = widget

    Market.resetCreateOffer()
    if Market.isItemSelected() then
        selectedItem:setItem(selectedItem.item.displayItem)
        nameLabel:setText(selectedItem.item.marketData.name)
        clearOffers()

        Market.enableCreateOffer(true) -- update offer types
        MarketProtocol.sendMarketBrowse(MarketRequest.BrowseItem, selectedItem.item.marketData.tradeAs) -- send browsed msg
    else
        Market.clearSelectedItem()
    end
end

local function updateBalance(balance)
    local balance = tonumber(balance)
    if not balance then
        return
    end

    if balance < 0 then
        balance = 0
    end
    information.balance = balance

    balanceLabel:setText('Balance: ' .. balance .. ' gold')
    balanceLabel:resizeToText()
end

local function updateFee(price, amount)
    fee = math.ceil(price / 100 * amount)
    if fee < 20 then
        fee = 20
    elseif fee > 1000 then
        fee = 1000
    end
    feeLabel:setText('Fee: ' .. fee)
    feeLabel:resizeToText()
end

local function destroyAmountWindow()
    if amountWindow then
        amountWindow:destroy()
        amountWindow = nil
    end
end

local function cancelMyOffer(actionType)
    local offer = selectedMyOffer[actionType]
    g_game.cancelMarketOffer(offer:getTimeStamp(), offer:getCounter())
    Market.refreshMyOffers()
end

local function openAmountWindow(callback, actionType, actionText)
    if not Market.isOfferSelected(actionType) then
        return
    end

    amountWindow = g_ui.createWidget('AmountWindow', rootWidget)
    amountWindow:lock()

    local offer = selectedOffer[actionType]
    local item = offer:getItem()

    local maximum = offer:getAmount()
    if actionType == MarketAction.Sell then
        local depot = Market.getDepotCount(item:getId())
        if maximum > depot then
            maximum = depot
        end
    else
        maximum = math.min(maximum, math.floor(information.balance / offer:getPrice()))
    end

    if item:isStackable() then
        maximum = math.min(maximum, MarketMaxAmountStackable)
    else
        maximum = math.min(maximum, MarketMaxAmount)
    end

    local itembox = amountWindow:getChildById('item')
    itembox:setItemId(item:getId())

    local scrollbar = amountWindow:getChildById('amountScrollBar')
    scrollbar:setText(offer:getPrice() .. 'gp')

    scrollbar.onValueChange = function(widget, value)
        widget:setText((value * offer:getPrice()) .. 'gp')
        itembox:setText(value)
    end

    scrollbar:setRange(1, maximum)
    scrollbar:setValue(1)

    local okButton = amountWindow:getChildById('buttonOk')
    if actionText then
        okButton:setText(actionText)
    end

    local okFunc = function()
        local counter = offer:getCounter()
        local timestamp = offer:getTimeStamp()
        callback(scrollbar:getValue(), timestamp, counter)
        destroyAmountWindow()
    end

    local cancelButton = amountWindow:getChildById('buttonCancel')
    local cancelFunc = function()
        destroyAmountWindow()
    end

    amountWindow.onEnter = okFunc
    amountWindow.onEscape = cancelFunc

    okButton.onClick = okFunc
    cancelButton.onClick = cancelFunc
end

local function onSelectSellOffer(table, selectedRow, previousSelectedRow)
    updateBalance()
    for _, offer in pairs(marketOffers[MarketAction.Sell]) do
        if offer:isEqual(selectedRow.ref) then
            selectedOffer[MarketAction.Buy] = offer
        end
    end

    local offer = selectedOffer[MarketAction.Buy]
    if offer then
        local price = offer:getPrice()
        if price > information.balance then
            balanceLabel:setColor('#b22222') -- red
            buyButton:setEnabled(false)
        else
            local slice = (information.balance / 2)
            if (price / slice) * 100 <= 40 then
                color = '#008b00' -- green
            elseif (price / slice) * 100 <= 70 then
                color = '#eec900' -- yellow
            else
                color = '#ee9a00' -- orange
            end
            balanceLabel:setColor(color)
            buyButton:setEnabled(true)
        end
    end
end

local function onSelectBuyOffer(table, selectedRow, previousSelectedRow)
    updateBalance()
    for _, offer in pairs(marketOffers[MarketAction.Buy]) do
        if offer:isEqual(selectedRow.ref) then
            selectedOffer[MarketAction.Sell] = offer
            if Market.getDepotCount(offer:getItem():getId()) > 0 then
                sellButton:setEnabled(true)
            else
                sellButton:setEnabled(false)
            end
        end
    end
end

local function onSelectMyBuyOffer(table, selectedRow, previousSelectedRow)
    for _, offer in pairs(marketOffers[MarketAction.Buy]) do
        if offer:isEqual(selectedRow.ref) then
            selectedMyOffer[MarketAction.Buy] = offer
            buyCancelButton:setEnabled(true)
        end
    end
end

local function onSelectMySellOffer(table, selectedRow, previousSelectedRow)
    for _, offer in pairs(marketOffers[MarketAction.Sell]) do
        if offer:isEqual(selectedRow.ref) then
            selectedMyOffer[MarketAction.Sell] = offer
            sellCancelButton:setEnabled(true)
        end
    end
end

local function onChangeCategory(combobox, option)
    local id = getMarketCategoryId(option)
    if id == MarketCategory.MetaWeapons then
        -- enable and load weapons filter/items
        subCategoryList:setEnabled(true)
        slotFilterList:setEnabled(true)
        local subId = getMarketCategoryId(subCategoryList:getCurrentOption().text)
        Market.loadMarketItems(subId)
    else
        subCategoryList:setEnabled(false)
        slotFilterList:setEnabled(false)
        Market.loadMarketItems(id) -- load standard filter
    end
end

local function onChangeSubCategory(combobox, option)
    Market.loadMarketItems(getMarketCategoryId(option))
    slotFilterList:clearOptions()

    local subId = getMarketCategoryId(subCategoryList:getCurrentOption().text)
    local slots = MarketCategoryWeapons[subId].slots
    for _, slot in pairs(slots) do
        if table.haskey(MarketSlotFilters, slot) then
            slotFilterList:addOption(MarketSlotFilters[slot])
        end
    end
    slotFilterList:setEnabled(true)
end

local function onChangeSlotFilter(combobox, option)
    Market.updateCurrentItems()
end

local function onChangeOfferType(combobox, option)
    local item = selectedItem.item
    local maximum = item.thingType:isStackable() and MarketMaxAmountStackable or MarketMaxAmount

    if option == 'Sell' then
        maximum = math.min(maximum, Market.getDepotCount(item.marketData.tradeAs))
        amountEdit:setMaximum(maximum)
    else
        amountEdit:setMaximum(maximum)
    end
end

local function onTotalPriceChange()
    local amount = amountEdit:getValue()
    local totalPrice = totalPriceEdit:getValue()
    local piecePrice = math.floor(totalPrice / amount)

    piecePriceEdit:setValue(piecePrice, true)
    if Market.isItemSelected() then
        updateFee(piecePrice, amount)
    end
end

local function onPiecePriceChange()
    local amount = amountEdit:getValue()
    local totalPrice = totalPriceEdit:getValue()
    local piecePrice = piecePriceEdit:getValue()

    totalPriceEdit:setValue(piecePrice * amount, true)
    if Market.isItemSelected() then
        updateFee(piecePrice, amount)
    end
end

local function onAmountChange()
    local amount = amountEdit:getValue()
    local piecePrice = piecePriceEdit:getValue()
    local totalPrice = piecePrice * amount

    totalPriceEdit:setValue(piecePrice * amount, true)
    if Market.isItemSelected() then
        updateFee(piecePrice, amount)
    end
end

local function onMarketMessage(messageMode, message)
    Market.displayMessage(message)
end

local function initMarketItems()
    for c = MarketCategory.First, MarketCategory.Last do
        marketItems[c] = {}
    end

    -- save a list of items which are already added
    local itemSet = {}

    -- populate all market items
    local types = g_things.findThingTypeByAttr(ThingAttrMarket, 0)
    for i = 1, #types do
        local itemType = types[i]

        local item = Item.create(itemType:getId())
        if item then
            local marketData = itemType:getMarketData()
            if not table.empty(marketData) and not itemSet[marketData.tradeAs] then
                -- Some items use a different sprite in Market
                item:setId(marketData.showAs)

                -- create new marketItem block
                local marketItem = {
                    displayItem = item,
                    thingType = itemType,
                    marketData = marketData
                }

                -- add new market item
                if not marketItems[marketData.category] then
                    marketItems[marketData.category] = {}
                end

                table.insert(marketItems[marketData.category], marketItem)
                itemSet[marketData.tradeAs] = true
            end
        end
    end
end

local function initInterface()
    -- TODO: clean this up
    -- setup main tabs
    mainTabBar = marketWindow:getChildById('mainTabBar')
    mainTabBar:setContentWidget(marketWindow:getChildById('mainTabContent'))

    -- setup 'Market Offer' section tabs
    marketOffersPanel = g_ui.loadUI('ui/marketoffers')
    g_mouse.bindPress(mainTabBar:addTab(tr('Market Offers'), marketOffersPanel), function()
        lastKnownTab = 'market offers'
        if os.time() > refreshTimeout + 1 then
            refreshTimeout = os.time()
            Market.refreshOffers()
        else
            if updateEvent then
                removeEvent(updateEvent)
                updateEvent = nil
            end
            updateEvent = scheduleEvent(function()
                Market.refreshMyOffers()
                refreshTimeout = os.time()
            end, 500)
        end
    end, MouseLeftButton)

    selectionTabBar = marketOffersPanel:getChildById('leftTabBar')
    selectionTabBar:setContentWidget(marketOffersPanel:getChildById('leftTabContent'))

    browsePanel = g_ui.loadUI('ui/marketoffers/browse')
    selectionTabBar:addTab(tr('Browse'), browsePanel)

    -- Currently not used
    -- "Reserved for more functionality later"
    -- overviewPanel = g_ui.loadUI('ui/marketoffers/overview')
    -- selectionTabBar:addTab(tr('Overview'), overviewPanel)

    displaysTabBar = marketOffersPanel:getChildById('rightTabBar')
    displaysTabBar:setContentWidget(marketOffersPanel:getChildById('rightTabContent'))

    itemStatsPanel = g_ui.loadUI('ui/marketoffers/itemstats')
    displaysTabBar:addTab(tr('Statistics'), itemStatsPanel)

    itemDetailsPanel = g_ui.loadUI('ui/marketoffers/itemdetails')
    displaysTabBar:addTab(tr('Details'), itemDetailsPanel)

    itemOffersPanel = g_ui.loadUI('ui/marketoffers/itemoffers')
    displaysTabBar:addTab(tr('Offers'), itemOffersPanel)
    displaysTabBar:selectTab(displaysTabBar:getTab(tr('Offers')))

    -- setup 'My Offer' section tabs
    myOffersPanel = g_ui.loadUI('ui/myoffers')
    g_mouse.bindPress(mainTabBar:addTab(tr('My Offers'), myOffersPanel), function()
        lastKnownTab = lastOfferTab ~= nil and lastOfferTab or 'current offers'
        if os.time() > refreshTimeout + 1 then
            refreshTimeout = os.time()
            Market.refreshMyOffers()
        else
            if updateEvent then
                removeEvent(updateEvent)
                updateEvent = nil
            end
            updateEvent = scheduleEvent(function()
                Market.refreshMyOffers()
                refreshTimeout = os.time()
            end, 500)
        end
    end, MouseLeftButton)

    offersTabBar = myOffersPanel:getChildById('offersTabBar')
    offersTabBar:setContentWidget(myOffersPanel:getChildById('offersTabContent'))

    currentOffersPanel = g_ui.loadUI('ui/myoffers/currentoffers')
    g_mouse.bindPress(offersTabBar:addTab(tr('Current Offers'), currentOffersPanel), function()
        lastKnownTab, lastOfferTab = 'current offers'
        if os.time() > refreshTimeout + 1 then
            refreshTimeout = os.time()
            Market.refreshMyOffers()
        else
            if updateEvent then
                removeEvent(updateEvent)
                updateEvent = nil
            end
            updateEvent = scheduleEvent(function()
                Market.refreshMyOffers()
                refreshTimeout = os.time()
            end, 500)
        end
    end, MouseLeftButton)

    offerHistoryPanel = g_ui.loadUI('ui/myoffers/offerhistory')
    g_mouse.bindPress(offersTabBar:addTab(tr('Offer History'), offerHistoryPanel), function()
        lastKnownTab, lastOfferTab = 'offer history'
        if os.time() > refreshTimeout + 1 then
            refreshTimeout = os.time()
            Market.refreshMyOffers()
        else
            if updateEvent then
                removeEvent(updateEvent)
                updateEvent = nil
            end
            updateEvent = scheduleEvent(function()
                Market.refreshOfferHistory()
                refreshTimeout = os.time()
            end, 500)
        end
    end, MouseLeftButton)

    balanceLabel = marketWindow:getChildById('balanceLabel')

    -- setup offers
    buyButton = itemOffersPanel:getChildById('buyButton')
    buyButton.onClick = function()
        openAmountWindow(Market.acceptMarketOffer, MarketAction.Buy, 'Buy')
    end

    sellButton = itemOffersPanel:getChildById('sellButton')
    sellButton.onClick = function()
        openAmountWindow(Market.acceptMarketOffer, MarketAction.Sell, 'Sell')
    end

    -- setup selected item
    nameLabel = marketOffersPanel:getChildById('nameLabel')
    selectedItem = marketOffersPanel:getChildById('selectedItem')

    -- setup create new offer
    totalPriceEdit = marketOffersPanel:getChildById('totalPriceEdit')
    piecePriceEdit = marketOffersPanel:getChildById('piecePriceEdit')
    amountEdit = marketOffersPanel:getChildById('amountEdit')
    feeLabel = marketOffersPanel:getChildById('feeLabel')
    totalPriceEdit.onValueChange = onTotalPriceChange
    piecePriceEdit.onValueChange = onPiecePriceChange
    amountEdit.onValueChange = onAmountChange

    offerTypeList = marketOffersPanel:getChildById('offerTypeComboBox')
    offerTypeList.onOptionChange = onChangeOfferType

    anonymous = marketOffersPanel:getChildById('anonymousCheckBox')
    createOfferButton = marketOffersPanel:getChildById('createOfferButton')
    createOfferButton.onClick = Market.createNewOffer
    Market.enableCreateOffer(false)

    -- setup filters
    filterButtons[MarketFilters.Vocation] = browsePanel:getChildById('filterVocation')
    filterButtons[MarketFilters.Level] = browsePanel:getChildById('filterLevel')
    filterButtons[MarketFilters.Depot] = browsePanel:getChildById('filterDepot')
    filterButtons[MarketFilters.SearchAll] = browsePanel:getChildById('filterSearchAll')

    -- set filter default values
    clearFilters()

    -- hook filters
    for _, filter in pairs(filterButtons) do
        filter.onCheckChange = Market.updateCurrentItems
    end

    searchEdit = browsePanel:getChildById('searchEdit')
    categoryList = browsePanel:getChildById('categoryComboBox')
    subCategoryList = browsePanel:getChildById('subCategoryComboBox')
    slotFilterList = browsePanel:getChildById('slotComboBox')

    slotFilterList:addOption(MarketSlotFilters[255])
    slotFilterList:setEnabled(false)

    for i = MarketCategory.First, MarketCategory.Last do
        if i >= MarketCategory.Ammunition and i <= MarketCategory.WandsRods then
            subCategoryList:addOption(getMarketCategoryName(i))
        else
            categoryList:addOption(getMarketCategoryName(i))
        end
    end
    categoryList:addOption(getMarketCategoryName(255)) -- meta weapons
    categoryList:setCurrentOption(getMarketCategoryName(MarketCategory.First))
    subCategoryList:setEnabled(false)

    -- hook item filters
    categoryList.onOptionChange = onChangeCategory
    subCategoryList.onOptionChange = onChangeSubCategory
    slotFilterList.onOptionChange = onChangeSlotFilter

    -- setup tables
    buyOfferTable = itemOffersPanel:recursiveGetChildById('buyingTable')
    sellOfferTable = itemOffersPanel:recursiveGetChildById('sellingTable')
    detailsTable = itemDetailsPanel:recursiveGetChildById('detailsTable')
    buyStatsTable = itemStatsPanel:recursiveGetChildById('buyStatsTable')
    sellStatsTable = itemStatsPanel:recursiveGetChildById('sellStatsTable')
    buyOfferTable.onSelectionChange = onSelectBuyOffer
    sellOfferTable.onSelectionChange = onSelectSellOffer

    -- setup my offers
    buyMyOfferTable = currentOffersPanel:recursiveGetChildById('myBuyingTable')
    sellMyOfferTable = currentOffersPanel:recursiveGetChildById('mySellingTable')
    buyMyOfferTable.onSelectionChange = onSelectMyBuyOffer
    sellMyOfferTable.onSelectionChange = onSelectMySellOffer

    buyCancelButton = currentOffersPanel:getChildById('buyCancelButton')
    buyCancelButton.onClick = function()
        cancelMyOffer(MarketAction.Buy)
    end

    sellCancelButton = currentOffersPanel:getChildById('sellCancelButton')
    sellCancelButton.onClick = function()
        cancelMyOffer(MarketAction.Sell)
    end

    buyStatsTable:setColumnWidth({120, 270})
    sellStatsTable:setColumnWidth({120, 270})
    detailsTable:setColumnWidth({80, 330})

    buyOfferTable:setSorting(4, TABLE_SORTING_DESC)
    sellOfferTable:setSorting(4, TABLE_SORTING_ASC)

    buyMyOfferTable:setSorting(3, TABLE_SORTING_DESC)
    sellMyOfferTable:setSorting(3, TABLE_SORTING_DESC)

    -- setup my history
    buyMyHistoryTable = offerHistoryPanel:recursiveGetChildById('myHistoryBuyingTable')
    sellMyHistoryTable = offerHistoryPanel:recursiveGetChildById('myHistorySellingTable')

    buyMyHistoryTable:setSorting(3, TABLE_SORTING_DESC)
    sellMyHistoryTable:setSorting(3, TABLE_SORTING_DESC)
end

function init()
    g_ui.importStyle('market')
    g_ui.importStyle('ui/general/markettabs')
    g_ui.importStyle('ui/general/marketbuttons')
    g_ui.importStyle('ui/general/marketcombobox')
    g_ui.importStyle('ui/general/amountwindow')

    offerExhaust[MarketAction.Sell] = 10
    offerExhaust[MarketAction.Buy] = 20

    registerMessageMode(MessageModes.Market, onMarketMessage)

    protocol.initProtocol()
    connect(g_game, {
        onGameEnd = Market.reset,
        onMarketEnter = Market.onMarketEnter,
        onMarketBrowse = Market.onMarketBrowse,
        onMarketDetail = Market.onMarketDetail,
        onMarketReadOffer = Market.onMarketReadOffer,
        onMarketLeave = Market.onMarketLeave,
        onResourcesBalanceChange = Market.onResourcesBalanceChange
    })
    connect(g_game, {
        onGameEnd = Market.close
    })
    marketWindow = g_ui.createWidget('MarketWindow', rootWidget)
    marketWindow:hide()

    initInterface() -- build interface
end

function terminate()
    Market.close()

    unregisterMessageMode(MessageModes.Market, onMarketMessage)

    protocol.terminateProtocol()
    disconnect(g_game, {
        onGameEnd = Market.reset,
        onMarketEnter = Market.onMarketEnter,
        onMarketBrowse = Market.onMarketBrowse,
        onMarketDetail = Market.onMarketDetail,
        onMarketReadOffer = Market.onMarketReadOffer,
        onMarketLeave = Market.onMarketLeave,
        onResourcesBalanceChange = Market.onResourcesBalanceChange
    })
    disconnect(g_game, {
        onGameEnd = Market.close
    })

    destroyAmountWindow()
    marketWindow:destroy()

    Market = nil
end

function Market.reset()
    balanceLabel:setColor('#bbbbbb')
    categoryList:setCurrentOption(getMarketCategoryName(MarketCategory.First))
    searchEdit:setText('')
    clearFilters()
    clearMyOffers()
    if not table.empty(information) then
        Market.updateCurrentItems()
    end
end

function Market.displayMessage(message)
    if marketWindow:isHidden() then
        return
    end

    local infoBox = displayInfoBox(tr('Market Error'), message)
    infoBox:lock()
end

function Market.clearSelectedItem()
    if Market.isItemSelected() then
        Market.resetCreateOffer(true)
        offerTypeList:clearOptions()
        offerTypeList:setText('Please Select')
        offerTypeList:setEnabled(false)

        clearOffers()
        radioItemSet:selectWidget(nil)
        nameLabel:setText('No item selected.')
        selectedItem:setItem(nil)
        selectedItem.item = nil
        selectedItem.ref:setChecked(false)
        selectedItem.ref = nil

        detailsTable:clearData()
        buyStatsTable:clearData()
        sellStatsTable:clearData()

        Market.enableCreateOffer(false)
    end
end

function Market.isItemSelected()
    return selectedItem and selectedItem.item
end

function Market.isOfferSelected(type)
    return selectedOffer[type] and not selectedOffer[type]:isNull()
end

function Market.getDepotCount(itemId)
    return information.depotItems[itemId] and information.depotItems[itemId].itemCount or 0
end

function Market.enableCreateOffer(enable)
    offerTypeList:setEnabled(enable)
    totalPriceEdit:setEnabled(enable)
    piecePriceEdit:setEnabled(enable)
    amountEdit:setEnabled(enable)
    anonymous:setEnabled(enable)
    createOfferButton:setEnabled(enable)

    local prevAmountButton = marketOffersPanel:recursiveGetChildById('prevAmountButton')
    local nextAmountButton = marketOffersPanel:recursiveGetChildById('nextAmountButton')

    prevAmountButton:setEnabled(enable)
    nextAmountButton:setEnabled(enable)
end

function Market.close(notify)
    if notify == nil then
        notify = true
    end
    if not marketWindow:isHidden() then
        marketWindow:hide()
        marketWindow:unlock()
        modules.game_interface.getRootPanel():focus()
        Market.clearSelectedItem()
        Market.reset()
        if notify then
            g_game.leaveMarket()
        end
    end
end

function Market.incrementAmount()
    amountEdit:setValue(amountEdit:getValue() + 1)
end

function Market.decrementAmount()
    amountEdit:setValue(amountEdit:getValue() - 1)
end

function Market.updateCurrentItems()
    local id = getMarketCategoryId(categoryList:getCurrentOption().text)
    if id == MarketCategory.MetaWeapons then
        id = getMarketCategoryId(subCategoryList:getCurrentOption().text)
    end
    Market.loadMarketItems(id)
end

function Market.resetCreateOffer(resetFee)
    piecePriceEdit:setValue(1)
    totalPriceEdit:setValue(1)
    amountEdit:setValue(1)
    refreshTypeList()

    if resetFee then
        clearFee()
    else
        updateFee(0, 0)
    end
end

function Market.refreshItemsWidget(selectItem)
    local selectItem = selectItem or 0
    itemsPanel = browsePanel:recursiveGetChildById('itemsPanel')

    local layout = itemsPanel:getLayout()
    layout:disableUpdates()

    Market.clearSelectedItem()
    itemsPanel:destroyChildren()

    if radioItemSet then
        radioItemSet:destroy()
    end
    radioItemSet = UIRadioGroup.create()

    local select = nil
    for i = 1, #currentItems do
        local item = currentItems[i]
        local itemBox = g_ui.createWidget('MarketItemBox', itemsPanel)
        itemBox.onCheckChange = Market.onItemBoxChecked
        itemBox.item = item

        if selectItem > 0 and item.marketData.tradeAs == selectItem then
            select = itemBox
            selectItem = 0
        end

        local itemWidget = itemBox:getChildById('item')
        itemWidget:setItem(item.displayItem)

        local amount = Market.getDepotCount(item.marketData.tradeAs)
        if amount > 0 then
            itemWidget:setText(amount)
            itemWidget:setTextOffset(topoint('0 10'))
            itemBox:setTooltip('You have ' .. amount .. ' in your depot.')
        end

        radioItemSet:addWidget(itemBox)
    end

    if select then
        radioItemSet:selectWidget(select, false)
    end

    layout:enableUpdates()
    layout:update()
end

function Market.refreshOffers()
    if (not lastKnownTab or lastKnownTab == 'market offers') and Market.isItemSelected() then
        Market.onItemBoxChecked(selectedItem.ref)
    elseif lastKnownTab == 'current offers' then
        Market.refreshMyOffers()
    elseif lastKnownTab == 'offer history' then
        Market.refreshOfferHistory()
    end
end

function Market.refreshMyOffers()
    clearMyOffers()
    MarketProtocol.sendMarketBrowseMyOffers()
end

function Market.refreshOfferHistory()
    clearMyHistory()
    MarketProtocol.sendMarketBrowseOfferHistory()
end

function Market.loadMarketItems(category)
    clearItems()

    -- check search filter
    local searchFilter = searchEdit:getText()
    if searchFilter and searchFilter:len() > 2 then
        if filterButtons[MarketFilters.SearchAll]:isChecked() then
            category = MarketCategory.All
        end
    end

    if category == MarketCategory.All then
        -- loop all categories
        for category = MarketCategory.First, MarketCategory.Last do
            for i = 1, #marketItems[category] do
                local item = marketItems[category][i]
                if isItemValid(item, category, searchFilter) then
                    table.insert(currentItems, item)
                end
            end
        end
    else
        if not marketItems[category] then
            return
        end
        -- loop specific category
        if not marketItems[category] then
            return
        end
        for i = 1, #marketItems[category] do
            local item = marketItems[category][i]
            if isItemValid(item, category, searchFilter) then
                table.insert(currentItems, item)
            end
        end
    end

    Market.refreshItemsWidget()
end

function Market.createNewOffer()
    local type = offerTypeList:getCurrentOption().text
    if type == 'Sell' then
        type = MarketAction.Sell
    else
        type = MarketAction.Buy
    end

    if not Market.isItemSelected() then
        return
    end

    local spriteId = selectedItem.item.marketData.tradeAs

    local piecePrice = piecePriceEdit:getValue()
    local amount = amountEdit:getValue()
    local anonymous = anonymous:isChecked() and 1 or 0

    -- error checking
    local errorMsg = ''
    if type == MarketAction.Buy then
        if information.balance < ((piecePrice * amount) + fee) then
            errorMsg = errorMsg .. 'Not enough balance to create this offer.\n'
        end
    elseif type == MarketAction.Sell then
        if information.balance < fee then
            errorMsg = errorMsg .. 'Not enough balance to create this offer.\n'
        end
        if Market.getDepotCount(spriteId) < amount then
            errorMsg = errorMsg .. 'Not enough items in your depot to create this offer.\n'
        end
    end

    if piecePrice > piecePriceEdit.maximum then
        errorMsg = errorMsg .. 'Price is too high.\n'
    elseif piecePrice < piecePriceEdit.minimum then
        errorMsg = errorMsg .. 'Price is too low.\n'
    end

    if amount > amountEdit.maximum then
        errorMsg = errorMsg .. 'Amount is too high.\n'
    elseif amount < amountEdit.minimum then
        errorMsg = errorMsg .. 'Amount is too low.\n'
    end

    if amount * piecePrice > MarketMaxPrice then
        errorMsg = errorMsg .. 'Total price is too high.\n'
    end

    if information.totalOffers >= MarketMaxOffers then
        errorMsg = errorMsg .. 'You cannot create more offers.\n'
    end

    local timeCheck = os.time() - lastCreatedOffer
    if timeCheck < offerExhaust[type] then
        local waitTime = math.ceil(offerExhaust[type] - timeCheck)
        errorMsg = errorMsg .. 'You must wait ' .. waitTime .. ' seconds before creating a new offer.\n'
    end

    if errorMsg ~= '' then
        Market.displayMessage(errorMsg)
        return
    end

    local itemTier = Item.create(spriteId):getClassification()
    g_game.createMarketOffer(type, spriteId, itemTier, amount, piecePrice, anonymous)
    lastCreatedOffer = os.time()
    Market.resetCreateOffer()
end

function Market.acceptMarketOffer(amount, timestamp, counter)
    if timestamp > 0 and amount > 0 then
        g_game.acceptMarketOffer(timestamp, counter, amount)
        Market.refreshOffers()
    end
end

function Market.onItemBoxChecked(widget)
    if widget:isChecked() then
        updateSelectedItem(widget)
    end
end

-- protocol callback functions

function Market.onMarketEnter(depotItems, offers, balance, vocation)
    if not loaded then
        initMarketItems()
        loaded = true
    end
    if balance == -1 then
        local player = g_game.getLocalPlayer()
        if player then
            updateBalance(player:getTotalMoney())
        end
    else
        updateBalance(balance)
    end
    averagePrice = 0

    information.totalOffers = offers
    local player = g_game.getLocalPlayer()
    if player then
        information.player = player
    end
    if vocation == -1 then
        if player then
            information.vocation = player:getVocation()
        end
    else
        -- vocation must be compatible with < 950
        information.vocation = vocation
    end
    local depotItemsLua = {}
    if type(depotItems) == 'table' and #depotItems >= 1 then
        for i = 1, #depotItems do
            local itemId = depotItems[i][1]
            local count = depotItems[i][2]
            local itClass = depotItems[i][3]
            if g_game.getClientVersion() > 1281 then
                if itemId and count and tonumber(itClass) >= 0 then
                    depotItemsLua[itemId] = {
                        itemCount = count,
                        itemClass = itClass
                    }
                end
            else
                if itemId and count then
                    depotItemsLua[itemId] = {
                        itemCount = count
                    }
                end
            end
        end
    end

    -- set list of depot items
    information.depotItems = depotItemsLua

    -- update the items widget to match depot items
    if Market.isItemSelected() then
        local spriteId = selectedItem.item.marketData.tradeAs
        MarketProtocol.silent(true) -- disable protocol messages
        Market.refreshItemsWidget(spriteId)
        MarketProtocol.silent(false) -- enable protocol messages
    else
        Market.refreshItemsWidget()
    end

    if table.empty(currentItems) then
        Market.loadMarketItems(MarketCategory.First)
    end

    if g_game.isOnline() then
        marketWindow:lock()
        marketWindow:show()
    end
end

function Market.onMarketLeave()
    Market.close(false)
end

function Market.onMarketDetail(itemId, descriptions, purchaseStats, saleStats)
    updateDetails(itemId, descriptions, purchaseStats, saleStats)
end

MarketOffers2 = {}

function Market.onMarketBrowse(intOffers, nameOffers)
    local tmpOffers = MarketOffers2
    MarketOffers2 = {}
    updateOffers(tmpOffers)
end

function Market.onMarketReadOffer(action, amount, counter, itemId, playerName, price, state, timestamp, var)
    table.insert(MarketOffers2, MarketOffer.new({timestamp, counter}, action, Item.create(itemId), amount, price,
                                                playerName, state, var))
end

function Market.onResourcesBalanceChange(value, oldBalance, resourceType)
    if resourceType <= 1 then
        local player = g_game.getLocalPlayer()
        if player then
            updateBalance(player:getTotalMoney())
        end
    end
end
